/*  -*- mode: c++; coding: utf-8; c-file-style: "stroustrup"; -*-

    Copyright 2008 Asier Aguirre <asier.aguirre@gmail.com>
    This file is part of Intromit.

    Intromit is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    Intromit is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with Intromit.  If not, see <http://www.gnu.org/licenses/>.
*/

#include "config.hh"
#include "file_mng.hh"
#include "util.hh"

#include <string.h>
#include <ctype.h>
#include <errno.h>

// DEFINES
#define INC_PAIR      128

// CLASS CONFIG_PAIR

void config_pair::ctor()
{
    type=CONFIG_PAIR_UNKNOWN;
    key=value=NULL;
}

void config_pair::dtor()
{
    type=CONFIG_PAIR_UNKNOWN;
    if(key) key=(char*)realloc(key, 0);
    if(value) value=(char*)realloc(value, 0);
}

void config_pair::set_key(char *key_, int n)
{
    if(key) key=(char*)realloc(key, 0);
    if(key_)
    {
	if(n<0) key=strdup(key_);
	else
	{
	    key=(char*)malloc(n+1);
	    memcpy(key, key_, n);
	    key[n]=0;
	}
    }
}

void config_pair::set_key_lowercase(char *key_, int n)
{
    set_key(key_, n);
    if(key)
    {
	if(n<0) n=strlen(key);
	for(int i=0; i<n; i++) key[i]=tolower(key[i]);
    }
}

void config_pair::set_value(char *value_, int n)
{
    if(value) value=(char*)realloc(value, 0);
    if(value_)
    {
	if(n<0) value=strdup(value_);
	else
	{
	    value=(char*)malloc(n+1);
	    memcpy(value, value_, n);
	    value[n]=0;
	}
    }
}


// CLASS CONFIG

config::config()
{
    // config pairs
    util::ctor_vector(pair, n_pair, max_pair);

    // filename
    filename=NULL;

    // data
    data=NULL;
    n_data=0;
}

config::~config()
{
    // config pairs
    for(int i=0; i<n_pair; i++) pair[i].dtor();
    util::dtor_vector(pair, n_pair, max_pair);

    // filename
    if(filename) free(filename);

    // data
    if(data) data=(char*)realloc(data, 0);
    n_data=0;
}

bool config::init_filename(char *filename_)
{
    if(!filename_) MSG_R0("no filename supplied");

    // filename
    if(filename) free(filename);
    filename=strdup(filename_);

    // init data
    data=NULL;
    n_data=0;
    int max=0;

    // open file
    if(!file_mng::load_n_max_nf<char>(data, n_data, max, filename))
	MSG_R0("cannot load config file: %s: %s", filename, strerror(errno));
    if(n_data+1>max) data=(char*)realloc(data, n_data+1);
    data[n_data]=0;

    // parse
    bool dev=parse();

    // free data
    if(data) data=(char*)realloc(data, 0);
    n_data=0;

    return dev;
}

void config::init_pairs()
{
    // delete pairs
    for(int i=0; i<n_pair; i++) pair[i].dtor();
    util::dtor_vector(pair, n_pair, max_pair);
}

bool config::parse()
{
    int level=0;
    for(int i=0; i<n_data; i++)
    {
	char c=data[i];
	if(isspace(c)) continue; // remove blank spaces
	if(c=='#') {parse_comment(i); continue;} // remove comments
	if(c=='<')
	{
	    // tag
	    char *key, *value;
	    int is_end, n_key, n_value;
	    if(!parse_tag(++i, &is_end, &key, &n_key, &value, &n_value)) return false;
	    if(is_end)
	    {
		// search begining
		int j=n_pair-1;
		while(j>=0 && pair[j].get_level()!=level-1) j--;
		int n_begin;
		if(j<0 || n_key!=(n_begin=strlen(pair[j].get_key())) || strncasecmp(pair[j].get_key(), key, n_begin))
		    return parse_error(i, "unmatched block tag", (j<0)?NULL:pair[j].get_key());
		level--;
	    }
	    else
	    {
		int j=util::inc_vector(pair, n_pair, max_pair, INC_PAIR);
		pair[j].ctor();
		pair[j].set_type(CONFIG_PAIR_BLOCK);
		pair[j].set_key_lowercase(key, n_key);
		if(value) pair[j].set_value(value, n_value);
		pair[j].set_level(level);
		pair[j].set_line(get_line(i));
		level++;
	    }
	    continue;
	}
	if(isalpha(c))
	{
	    // variable
	    char *key, *value;
	    int n_key, n_value;
	    if(!parse_line(i, &key, &n_key, &value, &n_value)) return false;

	    int j=util::inc_vector(pair, n_pair, max_pair, INC_PAIR);
	    pair[j].ctor();
	    pair[j].set_type(CONFIG_PAIR_VARIABLE);
	    pair[j].set_key_lowercase(key, n_key);
	    if(value) pair[j].set_value(value, n_value);
	    pair[j].set_level(level);
	    pair[j].set_line(get_line(i));
	    continue;
	}
	return parse_error(i, "unexpected character");
    }
    if(level) return parse_error(n_data-1, "unclosed block tags");
    return true;
}

bool config::parse_line(int &i, char **key, int *n_key, char **value, int *n_value)
{
    *key=&data[i]; *n_key=i; // start of key
    while(isalnum(data[i]) || data[i]=='_' || data[i]=='-') i++; // traverse name
    *n_key=i-*n_key; // end of key
    if(!isblank(data[i]) && data[i]!='#' && data[i]!='\n' && data[i]!='\r' && data[i]!='\\')
	return parse_error(i, "invalid character for line key");
    while(isblank(data[i]) || data[i]=='\\') // multiline remove blank before value
    {
	if(data[i]=='\\')
	{
	    if(data[i+1]=='\n') i++; else break;
	}
	i++;
    }
    if(data[i]!='\n' && data[i]!='#')
    {	// there is value
	*value=&data[i]; *n_value=i; // start of value
	while(i<n_data && data[i]!='\n' && data[i]!='#')
	{
	    if(data[i]=='<' || data[i]=='>') return parse_error(i, "characteres < or > not allowed for value");
	    if(data[i]=='\"') parse_string(++i);
	    if(data[i]=='\\')
	    {
		if(data[i+1]=='\n') data[i++]=' '; // multiline: remove '\\' and pass to next line
		if(data[i+1]=='#') data[i++]=' '; // substitute \# by #
	    }
	    i++;
	}
	int j=i-1;
	while(isblank(data[j]) || data[j]=='\n' || data[j]=='\r') j--; j++; // remove last spaces
	*n_value=j-*n_value;

	while(*n_value && isblank(**value)) {(*value)++; (*n_value)--;}
    }
    else *value=NULL;
    i--;
    return true;
}

bool config::parse_tag(int &i, int *is_end, char **key, int *n_key, char **value, int *n_value)
{
    if(data[i]!='/') *is_end=0; else {i++; *is_end=1;}
    while(isblank(data[i]) || data[i]=='\n' || data[i]=='\r') i++; // first spaces
    if(!isalpha(data[i])) return parse_error(i, "tag name must start by alpha character");
    *key=&data[i]; *n_key=i;
    while(isalnum(data[i]) || data[i]=='_' || data[i]=='-') i++; // traverse name
    *n_key=i-*n_key;
    if(!isblank(data[i]) && data[i]!='>' && data[i]!='\n' && data[i]!='\r' && data[i]!='#')
	return parse_error(i, "invalid character for tag key");
    while(isblank(data[i]) || data[i]=='\n' || data[i]=='\r') i++; // intermediate spaces
    if(data[i]=='#') parse_comment(++i);
    if(data[i]!='>')
    {
	*value=&data[i]; *n_value=i;
	while(i<n_data && data[i]!='>')
	{
	    if(data[i]=='<') return parse_error(i, "character < not allowed for tag");
	    if(data[i]=='\"') parse_string(++i);
	    if(data[i]=='#') parse_comment(++i);
	    i++;
	}
	if(i==n_data) return parse_error(i, "unfinished tag");
	int j=i-1;
	while(isblank(data[j]) || data[j]=='\n' || data[j]=='\r') j--; j++;
	*n_value=j-*n_value;
    }
    else *value=NULL;
    return true;
}

void config::parse_comment(int &i)
{
    while(data[++i]!='\n');
}

void config::parse_string(int &i)
{
    while(i<n_data && data[i]!='\"') i++;
}

bool config::parse_error(int pos, const char *error, char *key)
{
    int line, col;
    line=get_line(pos, &col);

    MSG_NEOL("config file error (line %d col %d): ", line, col);
    fprintf(stderr, "%s", error);
    if(key) fprintf(stderr, ": %s", key);
    fprintf(stderr, "\n");
    return false;
}

int config::get_line(int pos, int *col_)
{
    int col=-1, line=0;
    for(int i=0; ; i++)
    {
	if(!i || data[i-1]=='\n') {line++; col=0;}
	if(i==pos) break;
	if(data[i]!='\t') col++;
	else col=(col+8)&(-8); // goto next tab stop
    }
    if(col_) *col_=col;
    return line;
}

bool config::search_block(char *key, char *value, int *ini_, int *end_, int from)
{
    if(from>=n_pair || !key) return false;
    int level=pair[from].get_level();

    int ini=-1, i;
    for(i=from; i<n_pair; i++)
    {
	if(pair[i].get_level()<level)
	{
	    if(ini<0) return false; // not found
	    else break; // found
	}

	if(ini<0 && !strcmp(key, pair[i].get_key()) &&
	   (value==NULL || (pair[i].get_value() && !strcmp(value, pair[i].get_value()))))
	{
	    // found start
	    level=pair[i].get_level()+1;
	    ini=i;
	}
    }
    if(ini<0) return false;
    if(ini_) *ini_=ini;
    if(end_) *end_=i;
    return true;
}

int config::skip_block(int i_block)
{
    if(i_block<0 || i_block>=n_pair || pair[i_block].get_type()!=CONFIG_PAIR_BLOCK)
    {
	ERROR("bad i_block for a block skipping");
	return -1;
    }
    int level=pair[i_block].get_level();
    while(pair[++i_block].get_level()>level);
    return i_block;
}

void config::dump(char *filename)
{
    FILE *arch=stdout;
    if(filename && !(arch=fopen(filename, "w"))) MSG_R("%s: dump configuration: %s", filename, strerror(errno));

    int max_level=0;
    for(int i=0; i<n_pair; i++) if(max_level<pair[i].get_level()) max_level=pair[i].get_level();

    fprintf(arch, "Config. file:     %s\n"
                  "No. of variables: %d\n"
	          "Max. level:       %d\n\n", this->filename, n_pair, max_level);
    fprintf(arch, "%5s %5s %4s %4s %-20s %*s%s\n", "#####", "LINE", "TYPE", "LVL", "KEY", max_level*2, "", "VALUE");
    for(int i=0; i<n_pair; i++)
    {
	fprintf(arch, "%5d %5d %4s %4d %*s%-20s %*s%s\n", i,
		pair[i].get_line(),
		pair[i].get_type()?"VAR":"BLK",
		pair[i].get_level(),
		pair[i].get_level()*2, "",
		pair[i].get_key(),
		(max_level-pair[i].get_level())*2, "",
		pair[i].get_value());
    }
}

int config::get_parent(int i_pair)
{
    int i_pair_parent=-1;
    for(int i=i_pair-1; i>=0; i--)
	if(pair[i].get_level()<pair[i_pair].get_level())
	{
	    i_pair_parent=i;
	    break;
	}
    return i_pair_parent;
}

bool config::error_invalid_key(int i_pair, const char **valid_keys)
{
    int i_pair_parent=get_parent(i_pair);
    MSG("%s:%d: invalid key \"%s\" inside \"%s\" block",
	filename, pair[i_pair].get_line(),
	pair[i_pair].get_key(), i_pair_parent<0?"<no parent>":pair[i_pair_parent].get_key());
    if(valid_keys)
    {
	MSG("valid keys are:");
	for(int i=0; valid_keys[i]; i++) MSG("\t%s", valid_keys[i]);
    }
    return false;
}

bool config::error_invalid_position(int i_pair, const char *required_position)
{
    int i_pair_parent=get_parent(i_pair);
    MSG("%s:%d: bad key \"%s\" position inside \"%s\" block",
	filename, pair[i_pair].get_line(),
	pair[i_pair].get_key(), i_pair_parent<0?"<no parent>":pair[i_pair_parent].get_key());
    MSG_R0("\trequired position: %s", required_position);
}

bool config::error_invalid_type(int i_pair)
{
    int i_pair_parent=get_parent(i_pair);
    MSG_R0("%s:%d: key \"%s\" inside \"%s\" block should be %s type",
	   filename, pair[i_pair].get_line(),
	   pair[i_pair].get_key(), i_pair_parent<0?"<no parent>":pair[i_pair_parent].get_key(),
	   pair[i_pair].get_type()?"block":"variable");
}

bool config::error_no_value(int i_pair)
{
    int i_pair_parent=get_parent(i_pair);
    MSG_R0("%s:%d: key \"%s\" inside \"%s\" has no value",
	   filename, pair[i_pair].get_line(),
	   pair[i_pair].get_key(), i_pair_parent<0?"<no parent>":pair[i_pair_parent].get_key());
}

bool config::error_value(int i_pair)
{
    int i_pair_parent=get_parent(i_pair);
    MSG_R0("%s:%d: key \"%s\" inside \"%s\" has a value (unexpectedly)",
	   filename, pair[i_pair].get_line(),
	   pair[i_pair].get_key(), i_pair_parent<0?"<no parent>":pair[i_pair_parent].get_key());
}

bool config::error_no_id(int i_pair)
{
    int i_pair_parent=get_parent(i_pair);
    MSG_R0("%s:%d: key \"%s\" inside \"%s\" has no id",
	   filename, pair[i_pair].get_line(),
	   pair[i_pair].get_key(), i_pair_parent<0?"<no parent>":pair[i_pair_parent].get_key());
}

bool config::error_no_section(const char *section)
{
    MSG_R0("%s: empty or no %s section", filename, section);
}

bool config::check_type(int i_pair, config_pair_type type)
{
    if(pair[i_pair].get_type()!=type) return error_invalid_type(i_pair);
    return true;
}

bool config::check_valued(int i_pair)
{
    if(!pair[i_pair].get_value())
    {
	if(pair[i_pair].get_type()==CONFIG_PAIR_VARIABLE)
	    return error_no_value(i_pair);
	else
	    return error_no_id(i_pair);
    }
    return true;
}

bool config::check_valued_variable(int i_pair)
{
    if(!check_type(i_pair, CONFIG_PAIR_VARIABLE)) return false;
    if(!pair[i_pair].get_value()) return error_no_value(i_pair);
    return true;
}

bool config::check_valued_block(int i_pair)
{
    if(!check_type(i_pair, CONFIG_PAIR_BLOCK)) return false;
    if(!pair[i_pair].get_value()) return error_no_id(i_pair);
    return true;
}

bool config::check_unvalued_variable(int i_pair)
{
    if(!check_type(i_pair, CONFIG_PAIR_VARIABLE)) return false;
    if(pair[i_pair].get_value()) return error_value(i_pair);
    return true;
}
